namespace Jsl.FxCop
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.Contracts;
    using System.Linq;
    using Microsoft.FxCop.Sdk;

    /// <summary>
    /// Utility methods to make working with Types and TypeNodes easier.
    /// </summary>
    public static class TypeNodeHelper
    {
        #region Public Methods
        /// <summary>
        /// Enumerates the descendants of the type.
        /// </summary>
        /// <param name="type">The type.</param>
        /// <returns>The descendants of the type.</returns>
        public static IEnumerable<TypeNode> DescendantTypesIncludingSelf(this TypeNode type)
        {
            var descendantType = type;
            while (descendantType != null)
            {
                yield return descendantType;

                descendantType = descendantType.BaseType;
            }
        }

        /// <summary>
        /// Gets the common interfaces.
        /// </summary>
        /// <param name="interface1">The first interface interface.</param>
        /// <param name="interface2">The interface to join in.</param>
        /// <returns>The common interfaces.</returns>
        public static IEnumerable<InterfaceNode> GetCommonInterfaces(
            this InterfaceNode interface1,
            InterfaceNode interface2)
        {
            return new InterfaceNode[] { interface1 }.GetCommonInterfaces(interface2);
        }

        /// <summary>
        /// Gets the common interfaces.
        /// </summary>
        /// <param name="interfaces">The interfaces.</param>
        /// <param name="interface2">The interface to join in.</param>
        /// <returns>The common interfaces.</returns>
        public static IEnumerable<InterfaceNode> GetCommonInterfaces(
            this IEnumerable<InterfaceNode> interfaces,
            InterfaceNode interface2)
        {
            var uniqueInterfaces = new UniqueTypeCollection<InterfaceNode>();
            foreach (var interface1 in interfaces)
            {
                if (interface1.IsAssignableTo(interface2))
                {
                    uniqueInterfaces.Add(interface2);
                }
                else
                {
                    // Build up all the matches that we find.
                    var singleInterface1 = new InterfaceNode[] { interface1 };
                    foreach (var childInterface2 in interface2.Interfaces)
                    {
                        foreach (var descendantInterface2 in singleInterface1.GetCommonInterfaces(childInterface2))
                        {
                            uniqueInterfaces.Add(descendantInterface2);
                        }
                    }
                }
            }

            return uniqueInterfaces;
        }

        /// <summary>
        /// Gets the common class that the two classes have in common.
        /// </summary>
        /// <param name="type1">The first type.</param>
        /// <param name="type2">The second type.</param>
        /// <returns>The common class that the two classes have in common. Returns <see cref="System.Object"/> if no common base.</returns>
        /// <exception cref="InvalidOperationException">Thrown if either type is actually an interface.</exception>
        public static TypeNode GetCommonClass(this TypeNode type1, TypeNode type2)
        {
            // We can only process classes, not interfaces.
            if ((type1 is InterfaceNode) || (type2 is InterfaceNode))
            {
                throw new InvalidOperationException("Only classes can be processed.");
            }

            // If the types are synonymous (or the same) then just return the best one.
            var bestSynonymousType = type1.GetBestSynonymousType(type2);
            if (bestSynonymousType != null)
            {
                return bestSynonymousType;
            }

            // If we can find a common base type then return it. Otherwise return System.Object.
            var commonBaseType =
                (from type1Descendant in type1.DescendantTypesIncludingSelf()
                 from type2Descendant in type2.DescendantTypesIncludingSelf()
                 where object.ReferenceEquals(type1Descendant, type2Descendant)
                 select type1Descendant)
                .FirstOrDefault();
            return (commonBaseType != null) ? commonBaseType : FrameworkTypes.Object;
        }

        public static TypeNode GetBestSynonymousType(this TypeNode type1, TypeNode type2)
        {
            // If the types are exactly the same just return one of them.
            if (object.ReferenceEquals(type1, type2))
            {
                return type1;
            }

            // An Int32 is just a Boolean.
            if (((type1 == FrameworkTypes.Boolean) && (type2 == FrameworkTypes.Int32))
                || ((type1 == FrameworkTypes.Int32) && (type2 == FrameworkTypes.Boolean)))
            {
                return FrameworkTypes.Boolean;
            }

            // Prefer enum to ValueType.
            if ((type1 is EnumNode) && (type2.FullName == WellKnownTypeNames.ValueType))
            {
                return type1;
            }
            else if ((type1.FullName == WellKnownTypeNames.ValueType) && (type2 is EnumNode))
            {
                return type2;
            }

            // Prefer enum to Int32.
            if ((type1 is EnumNode) && (type2.FullName == WellKnownTypeNames.Int32))
            {
                return type1;
            }
            else if ((type1.FullName == WellKnownTypeNames.Int32) && (type2 is EnumNode))
            {
                return type2;
            }

            // Prefer Char to UInt16.
            if ((type1.FullName == WellKnownTypeNames.Char) && (type2.FullName == WellKnownTypeNames.UInt16))
            {
                return type1;
            }
            else if ((type2.FullName == WellKnownTypeNames.Char) && (type1.FullName == WellKnownTypeNames.UInt16))
            {
                return type2;
            }

            return null;
        }

        public static bool IsSynonymousType(this TypeNode type1, TypeNode type2)
        {
            return type1.GetBestSynonymousType(type2) != null;
        }

        public static ClassNode GetClassNode(this Type targetClass)
        {
            return (ClassNode)targetClass.GetTypeNode();
        }

        public static InterfaceNode GetInterfaceNode(this Type targetInterface)
        {
            return (InterfaceNode)targetInterface.GetTypeNode();
        }

        public static Struct GetStruct(this Type targetStruct)
        {
            return (Struct)targetStruct.GetTypeNode();
        }

        /// <summary>
        /// Gets the type node.
        /// </summary>
        /// <param name="targetType">Type of the target.</param>
        /// <returns>The <see cref="TypeNode"/> found.</returns>
        public static TypeNode GetTypeNode(this Type targetType)
        {
            if (targetType == null)
            {
                throw new ArgumentNullException("targetType");
            }

            Contract.Ensures(Contract.Result<TypeNode>() != null);

            if (targetType.DeclaringType != null)
            {
                var declaringType = targetType.DeclaringType.GetTypeNode();
                return
                    (from type in declaringType.Members.OfType<TypeNode>()
                     where type.Name.Name == targetType.Name
                     select type)
                    .First();
            }
            else
            {
                return targetType
                    .Assembly
                    .GetAssemblyNode()
                    .GetType(Identifier.For(targetType.Namespace), Identifier.For(targetType.Name));
            }
        }

        /// <summary>
        /// Gets the type node.
        /// </summary>
        /// <param name="module">The module, usually the main assembly.</param>
        /// <param name="typeName">Name of the type.</param>
        /// <returns>The <see cref="TypeNode"/> found.</returns>
        public static TypeNode GetTypeNode(this ModuleNode module, string typeName)
        {
            if (module == null)
            {
                throw new ArgumentNullException("module");
            }

            if (string.IsNullOrEmpty(typeName))
            {
                throw new ArgumentNullException("typeName");
            }

            // Break up the parts of the full type name into the namespace and name.
            var lastDelimiter = typeName.LastIndexOf(Type.Delimiter);
            string @namespace;
            string name;
            if (lastDelimiter >= 0)
            {
                @namespace = typeName.Substring(0, lastDelimiter);
                name = typeName.Substring(lastDelimiter + 1);
            }
            else
            {
                @namespace = string.Empty;
                name = typeName;
            }

            return module.GetType(Identifier.For(@namespace), Identifier.For(name));
        }

        /// <summary>
        /// Returns <c>true</c> if the typeNode can be assigned to a variable of the specified type.
        /// </summary>
        /// <param name="typeNode">The typeNode to check.</param>
        /// <param name="type">The base type.</param>
        /// <returns><c>true</c> if the typeNode can be assigned to a variable of the specified type.</returns>
        /// <remarks>
        /// This basically checks to see if the typeNode is, or derives off of, the specified type.
        /// </remarks>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:ValidateArgumentsOfPublicMethods", Justification = "Handled in inner method.")]
        public static bool IsAssignableTo(this TypeNode typeNode, Type type)
        {
            return TypeNodeHelper.IsAssignableTo(typeNode, type.FullName);
        }

        /// <summary>
        /// Returns <c>true</c> if the typeNode can be assigned to a variable of the specified type.
        /// </summary>
        /// <param name="typeNode">The typeNode to check.</param>
        /// <param name="typeFullName">The base type full name.</param>
        /// <returns><c>true</c> if the typeNode can be assigned to a variable of the specified type.</returns>
        /// <remarks>
        /// This basically checks to see if the typeNode is, or derives off of, the specified type.
        /// </remarks>
        public static bool IsAssignableTo(this TypeNode typeNode, string typeFullName)
        {
            if (typeNode == null)
            {
                throw new ArgumentNullException("typeNode");
            }

            while (typeNode != null)
            {
                if (typeNode.IsGeneric)
                {
                    if (typeNode.Template.FullName == typeFullName)
                    {
                        return true;
                    }
                }
                else
                {
                    if (typeNode.FullName == typeFullName)
                    {
                        return true;
                    }
                }

                typeNode = typeNode.BaseType;
            }

            return false;
        }

        /// <summary>
        /// Returns <c>true</c> if the typeNode can be assigned to a variable of any of the specified types.
        /// </summary>
        /// <param name="typeNode">The typeNode to check.</param>
        /// <param name="typeFullNames">The base type full name.</param>
        /// <returns><c>true</c> if the typeNode can be assigned to a variable of any of the specified type.</returns>
        /// <remarks>
        /// This basically checks to see if the typeNode is, or derives off of, the specified type.
        /// </remarks>
        public static bool IsAssignableTo(this TypeNode typeNode, params string[] typeFullNames)
        {
            if (typeFullNames == null)
            {
                throw new ArgumentNullException("typeFullNames");
            }

            foreach (string typeFullName in typeFullNames)
            {
                if (TypeNodeHelper.IsAssignableTo(typeNode, typeFullName))
                {
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Determines the <see cref="TypeNode"/> is a code generated.
        /// </summary>
        /// <param name="typeNode">The type node.</param>
        /// <returns><c>true</c> if <see cref="TypeNode"/> is a code generated.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "By design.")]
        public static bool IsGeneratedCode(this TypeNode typeNode)
        {
            if (typeNode == null)
            {
                throw new ArgumentNullException("typeNode");
            }

            Contract.EndContractBlock();
            Contract.Assume(typeNode.Attributes != null);

            return
                (from attribute in typeNode.Attributes
                 where attribute.Type.FullName == WellKnownTypeNames.GeneratedCodeAttribute
                 select attribute)
                .Any();
        }
        #endregion Public Methods
    }
}